﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.ComponentModel;
using System.Text.RegularExpressions;
using System.Reflection;
using System.Web.Mvc;

namespace MvcImproved
{
	internal static class DictionaryHelpers
	{
		public static IEnumerable<KeyValuePair<string, TValue>> FindKeysWithPrefix<TValue>(IDictionary<string, TValue> dictionary, string prefix)
		{
			TValue exactMatchValue;
			if (dictionary.TryGetValue(prefix, out exactMatchValue))
			{
				yield return new KeyValuePair<string, TValue>(prefix, exactMatchValue);
			}

			foreach (var entry in dictionary)
			{
				string key = entry.Key;

				if (key.Length <= prefix.Length)
				{
					continue;
				}

				if (!key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
				{
					continue;
				}

				char charAfterPrefix = key[prefix.Length];
				switch (charAfterPrefix)
				{
					case '[':
					case '.':
						yield return entry;
						break;
				}
			}
		}

		public static bool DoesAnyKeyHavePrefix<TValue>(IDictionary<string, TValue> dictionary, string prefix)
		{
			return FindKeysWithPrefix(dictionary, prefix).Any();
		}

	}

	internal class ValueProviderEnumerable : IEnumerable<KeyValuePair<string, ValueProviderResult>>
	{
		private IValueProvider _ValueProvider;

		public ValueProviderEnumerable(IValueProvider valueProvider)
		{
			_ValueProvider = valueProvider;
		}

		public IEnumerator<KeyValuePair<string, ValueProviderResult>> GetEnumerator()
		{
			return new ValueProviderEnumerator(_ValueProvider);
		}

		IEnumerator IEnumerable.GetEnumerator()
		{
			return GetEnumerator();
		}

		public class ValueProviderEnumerator : IEnumerator<KeyValuePair<string, ValueProviderResult>>
		{
			private IValueProvider _ValueProvider;
			private List<ValueProviderEnumerator> _Children;
			private IEnumerator<ValueProviderEnumerator> _ChildEnumerator;
			private IEnumerator<KeyValuePair<string, ValueProviderResult>> _ChildValuesEnumerator;
			private static FieldInfo namevalueCollectionValuesfield = typeof(NameValueCollectionValueProvider).GetField("_values", BindingFlags.Instance | BindingFlags.NonPublic);

			internal ValueProviderEnumerator(IValueProvider valueProvider)
			{
				_ValueProvider = valueProvider;
				Reset();
			}

			public void Reset ()
			{
				if (_ValueProvider is ValueProviderCollection)
				{
					_Children = new List<ValueProviderEnumerator>();
					foreach (IValueProvider childProvider in (ValueProviderCollection)_ValueProvider)
					{
						_Children.Add(new ValueProviderEnumerator(childProvider));
					}
				}
				else if (_ValueProvider is NameValueCollectionValueProvider)
					_ChildValuesEnumerator = ((IEnumerable<KeyValuePair<string, ValueProviderResult>>)namevalueCollectionValuesfield.GetValue(_ValueProvider)).GetEnumerator();
				else if (_ValueProvider is IDictionary<string, ValueProviderResult>)
					_ChildValuesEnumerator = ((IDictionary<string, ValueProviderResult>)_ValueProvider).GetEnumerator();
			}

			public bool MoveNext()
			{
				if (_Children != null)
				{
					//Initialize the child enumerator and move it to the first child
					if (_ChildEnumerator == null)
					{
						_ChildEnumerator = _Children.GetEnumerator();
						if (!_ChildEnumerator.MoveNext())
							return false;
					}

					//Go through children's children first, and if all out, move to next child (recursive)
					while (true)
					{
						if (_ChildEnumerator.Current.MoveNext())
							return true;
						else if (!_ChildEnumerator.MoveNext())
							return false;
					}
				}
				//Return an actual value
				else if (_ChildValuesEnumerator != null)
					return _ChildValuesEnumerator.MoveNext();

				return false;
			}

			public KeyValuePair<string, ValueProviderResult> Current
			{
				get
				{
					if (_ChildValuesEnumerator != null)
						return _ChildValuesEnumerator.Current;
					else
						return _ChildEnumerator.Current.Current;
				}
			}

			object IEnumerator.Current
			{
				get
				{
					return Current;
				}
			}

			public void Dispose()
			{
				_ChildEnumerator = null;
				_ChildValuesEnumerator = null;
				_ValueProvider = null;
				_Children = null;
			}
		}
	}

	public class DefaultModelBinderImproved : DefaultModelBinder
	{
		private readonly bool _ConvertEmptyStringsToNull;
		public DefaultModelBinderImproved(bool convertEmptyStringsToNull)
		{
			_ConvertEmptyStringsToNull = convertEmptyStringsToNull;
		}

		public DefaultModelBinderImproved()
			: this(false)
		{}

		/* Copied functions from DefaultModelBinder */
		public static Type ExtractGenericInterface(Type queryType, Type interfaceType)
		{
			Func<Type, bool> matchesInterface = t => t.IsGenericType && t.GetGenericTypeDefinition() == interfaceType;
			return (matchesInterface(queryType)) ? queryType : queryType.GetInterfaces().FirstOrDefault(matchesInterface);
		}

		public static bool IsNullableValueType(Type type)
		{
			return Nullable.GetUnderlyingType(type) != null;
		}

		public static bool TypeAllowsNullValue(Type type)
		{
			return (!type.IsValueType || IsNullableValueType(type));
		}

		private static bool VerifyValueUsability(ControllerContext controllerContext, ModelStateDictionary modelState, string modelStateKey, Type elementType, object value)
		{
			if (value == null && !TypeAllowsNullValue(elementType))
			{
				if (modelState.IsValidField(modelStateKey))
				{
					// a required entry field was left blank
					string message = GetValueRequiredResource(controllerContext);
					modelState.AddModelError(modelStateKey, message);
				}
				// we don't care about "you must enter a value" messages if there was an error
				return false;
			}

			return true;
		}

		private static string GetValueRequiredResource(ControllerContext controllerContext)
		{
			string resourceValue = null;
			if (!String.IsNullOrEmpty(ResourceClassKey) && (controllerContext != null) && (controllerContext.HttpContext != null))
			{
				// If the user specified a ResourceClassKey try to load the resource they specified.
				// If the class key is invalid, an exception will be thrown.
				// If the class key is valid but the resource is not found, it returns null, in which
				// case it will fall back to the MVC default error message.
				resourceValue = controllerContext.HttpContext.GetGlobalResourceObject(ResourceClassKey, "PropertyValueRequired", CultureInfo.CurrentUICulture) as string;
			}
			return resourceValue ?? MainResource.DefaultModelBinder_ValueRequired;
		}

		// This helper type is used because we're working with strongly-typed collections, but we don't know the Ts
		// ahead of time. By using the generic methods below, we can consolidate the collection-specific code in a
		// single helper type rather than having reflection-based calls spread throughout the DefaultModelBinder type.
		// There is a single point of entry to each of the methods below, so they're fairly simple to maintain.

		private static class CollectionHelpers
		{

			private static readonly MethodInfo _replaceCollectionMethod = typeof(CollectionHelpers).GetMethod("ReplaceCollectionImpl", BindingFlags.Static | BindingFlags.NonPublic);
			private static readonly MethodInfo _replaceDictionaryMethod = typeof(CollectionHelpers).GetMethod("ReplaceDictionaryImpl", BindingFlags.Static | BindingFlags.NonPublic);

			public static void ReplaceCollection(Type collectionType, object collection, object newContents)
			{
				MethodInfo targetMethod = _replaceCollectionMethod.MakeGenericMethod(collectionType);
				targetMethod.Invoke(null, new object[] { collection, newContents });
			}

			private static void ReplaceCollectionImpl<T>(ICollection<T> collection, IEnumerable newContents)
			{
				collection.Clear();
				if (newContents != null)
				{
					foreach (object item in newContents)
					{
						// if the item was not a T, some conversion failed. the error message will be propagated,
						// but in the meanwhile we need to make a placeholder element in the array.
						T castItem = (item is T) ? (T)item : default(T);
						collection.Add(castItem);
					}
				}
			}

			public static void ReplaceDictionary(Type keyType, Type valueType, object dictionary, object newContents)
			{
				MethodInfo targetMethod = _replaceDictionaryMethod.MakeGenericMethod(keyType, valueType);
				targetMethod.Invoke(null, new object[] { dictionary, newContents });
			}

			private static void ReplaceDictionaryImpl<TKey, TValue>(IDictionary<TKey, TValue> dictionary, IEnumerable<KeyValuePair<object, object>> newContents)
			{
				dictionary.Clear();
				foreach (var item in newContents)
				{
					// if the item was not a T, some conversion failed. the error message will be propagated,
					// but in the meanwhile we need to make a placeholder element in the dictionary.
					TKey castKey = (TKey)item.Key; // this cast shouldn't fail
					TValue castValue = (item.Value is TValue) ? (TValue)item.Value : default(TValue);
					dictionary[castKey] = castValue;
				}
			}

		}

		private static object ConvertProviderResult(ModelStateDictionary modelState, string modelStateKey, ValueProviderResult valueProviderResult, Type destinationType)
		{
			try
			{
				object convertedValue = valueProviderResult.ConvertTo(destinationType);
				return convertedValue;
			}
			catch (Exception ex)
			{
				modelState.AddModelError(modelStateKey, ex);
				return null;
			}
		}

		internal object BindSimpleModel(ControllerContext controllerContext, ModelBindingContext bindingContext, ValueProviderResult valueProviderResult) {
			bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

			// if the value provider returns an instance of the requested data type, we can just short-circuit
			// the evaluation and return that instance
			if (bindingContext.ModelType.IsInstanceOfType(valueProviderResult.RawValue)) {
				return valueProviderResult.RawValue;
			}

			// since a string is an IEnumerable<char>, we want it to skip the two checks immediately following
			if (bindingContext.ModelType != typeof(string)) {

				// conversion results in 3 cases, as below
				if (bindingContext.ModelType.IsArray) {
					// case 1: user asked for an array
					// ValueProviderResult.ConvertTo() understands array types, so pass in the array type directly
					object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType);
					return modelArray;
				}

				Type enumerableType = ExtractGenericInterface(bindingContext.ModelType, typeof(IEnumerable<>));
				if (enumerableType != null) {
					// case 2: user asked for a collection rather than an array
					// need to call ConvertTo() on the array type, then copy the array to the collection
					object modelCollection = CreateModel(controllerContext, bindingContext, bindingContext.ModelType);
					Type elementType = enumerableType.GetGenericArguments()[0];
					Type arrayType = elementType.MakeArrayType();
					object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, arrayType);

					Type collectionType = typeof(ICollection<>).MakeGenericType(elementType);
					if (collectionType.IsInstanceOfType(modelCollection)) {
						CollectionHelpers.ReplaceCollection(elementType, modelCollection, modelArray);
					}
					return modelCollection;
				}
			}

			// case 3: user asked for an individual element
			object model = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType);
			return model;
		}

		private static void AddValueRequiredMessageToModelState(ControllerContext controllerContext, ModelStateDictionary modelState, string modelStateKey, Type elementType, object value)
		{
			if (value == null && ! TypeAllowsNullValue(elementType) && modelState.IsValidField(modelStateKey))
			{
				modelState.AddModelError(modelStateKey, GetValueRequiredResource(controllerContext));
			}
		}

		/* End DefaultModelBinder Copied Functions */

		public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
		{
			if (! _ConvertEmptyStringsToNull)
				bindingContext.ModelMetadata.ConvertEmptyStringToNull = false;

			bool performedFallback = false;

			if (!String.IsNullOrEmpty(bindingContext.ModelName) && !bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
			{
				// We couldn't find any entry that began with the prefix. If this is the top-level element, fall back
				// to the empty prefix.
				if (bindingContext.FallbackToEmptyPrefix)
				{
					bindingContext = new ModelBindingContext()
					{
						ModelMetadata = bindingContext.ModelMetadata,
						ModelState = bindingContext.ModelState,
						PropertyFilter = bindingContext.PropertyFilter,
						ValueProvider = bindingContext.ValueProvider
					};
					performedFallback = true;
				}
				else
				{
					return null;
				}
			}

			// Simple model = int, string, etc.; determined by calling TypeConverter.CanConvertFrom(typeof(string))
			// or by seeing if a value in the request exactly matches the name of the model we're binding.
			// Complex type = everything else.
			if (!performedFallback)
			{
				ValueProviderResult vpResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
				if (vpResult != null)
				{
					return BindSimpleModel(controllerContext, bindingContext, vpResult);
				}
			}

			if (!bindingContext.ModelMetadata.IsComplexType)
			{
				return null;
			}

			return BindListOrDictionary(controllerContext, bindingContext);
		}

		private object BindListOrDictionary(ControllerContext controllerContext, ModelBindingContext bindingContext)
		{
			object model = bindingContext.Model;
			Type modelType = bindingContext.ModelType;

			// if we're being asked to create an array, create a list instead, then coerce to an array after the list is created
			if (model == null && modelType.IsArray)
			{
				Type elementType = modelType.GetElementType();
				Type listType = typeof(List<>).MakeGenericType(elementType);
				object collection = CreateModel(controllerContext, bindingContext, listType);

				ModelBindingContext arrayBindingContext = new ModelBindingContext()
				{
					ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => collection, listType),
					ModelName = bindingContext.ModelName,
					ModelState = bindingContext.ModelState,
					PropertyFilter = bindingContext.PropertyFilter,
					ValueProvider = bindingContext.ValueProvider
				};
				IList list = (IList)UpdateCollectionImproved(controllerContext, arrayBindingContext, elementType);

				if (list == null)
				{
					return null;
				}

				Array array = Array.CreateInstance(elementType, list.Count);
				list.CopyTo(array, 0);
				return array;
			}

			if (model == null)
			{
				model = CreateModel(controllerContext, bindingContext, modelType);
			}

			// special-case IDictionary<,> and ICollection<>
			Type dictionaryType = ExtractGenericInterface(modelType, typeof(IDictionary<,>));
			if (dictionaryType != null)
			{
				Type[] genericArguments = dictionaryType.GetGenericArguments();
				Type keyType = genericArguments[0];
				Type valueType = genericArguments[1];

				ModelBindingContext dictionaryBindingContext = new ModelBindingContext()
				{
					ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType),
					ModelName = bindingContext.ModelName,
					ModelState = bindingContext.ModelState,
					PropertyFilter = bindingContext.PropertyFilter,
					ValueProvider = bindingContext.ValueProvider
				};
				object dictionary = UpdateDictionaryImproved(controllerContext, dictionaryBindingContext, keyType, valueType);
				return dictionary;
			}

			Type enumerableType = ExtractGenericInterface(modelType, typeof(IEnumerable<>));
			if (enumerableType != null)
			{
				Type elementType = enumerableType.GetGenericArguments()[0];

				Type collectionType = typeof(ICollection<>).MakeGenericType(elementType);
				if (collectionType.IsInstanceOfType(model))
				{
					ModelBindingContext collectionBindingContext = new ModelBindingContext()
					{
						ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType),
						ModelName = bindingContext.ModelName,
						ModelState = bindingContext.ModelState,
						PropertyFilter = bindingContext.PropertyFilter,
						ValueProvider = bindingContext.ValueProvider
					};
					object collection = UpdateCollectionImproved(controllerContext, collectionBindingContext, elementType);
					return collection;
				}
			}

			//Fall back to default modelbinder
			return base.BindModel(controllerContext, bindingContext);
		}

		protected static string CreateSubIndexName(string prefix, string key)
		{
			return String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", prefix, key);
		}

		/* Copied from ValueProviderResult.ConvertSimpleType */
		internal object ConvertKey(CultureInfo culture, string value, Type destinationType)
		{
			TypeConverter converter = TypeDescriptor.GetConverter(destinationType);
			bool canConvertFrom = converter.CanConvertFrom(value.GetType());
			if (!canConvertFrom)
			{
				converter = TypeDescriptor.GetConverter(value.GetType());
			}
			if (!(canConvertFrom || converter.CanConvertTo(destinationType)))
			{
				string message = String.Format(CultureInfo.CurrentUICulture, MainResource.ValueProviderResult_NoConverterExists,
					value.GetType().FullName, destinationType.FullName);
				throw new InvalidOperationException(message);
			}

			object convertedValue = (canConvertFrom) ?
				 converter.ConvertFrom(null /* context */, culture, value) :
				 converter.ConvertTo(null /* context */, culture, value, destinationType);
			return convertedValue;
		}

		internal object UpdateDictionaryImproved(ControllerContext controllerContext, ModelBindingContext bindingContext, Type keyType, Type valueType)
		{
			// build up a list of items from the request
			List<KeyValuePair<object, object>> modelList = new List<KeyValuePair<object, object>>();
			HashSet<string> processedKey = new HashSet<string>();
			int prefixLength = bindingContext.ModelName.Length;
			Regex keyMatch = new Regex("^" + Regex.Escape(bindingContext.ModelName) + @"\[([^\]]+)\]", RegexOptions.IgnoreCase);
			
			IModelBinder keyBinder = Binders.GetBinder(keyType);
            IModelBinder valueBinder = Binders.GetBinder(valueType);

			IEnumerable<KeyValuePair<string, ValueProviderResult>> values = new ValueProviderEnumerable(bindingContext.ValueProvider);
			foreach (var entry in values)
			{
				if (entry.Key.Length <= prefixLength)
				{
					continue;
				}
				Match foundKey = keyMatch.Match(entry.Key);
				if (!foundKey.Success)
					continue;
				string currentKey = foundKey.Groups[1].Value;
				if (processedKey.Contains(currentKey))	//A form member of a complex object, already processed in a previous iteration
					continue;
				else
					processedKey.Add(currentKey);
				TypeConverter converter = TypeDescriptor.GetConverter(keyType);
				object thisKey = ConvertKey(null, currentKey, keyType);

				// bind the value
				string valueFieldKey = CreateSubIndexName(bindingContext.ModelName, currentKey);

                // bind the value
                ModelBindingContext valueBindingContext = new ModelBindingContext() {
                    ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, valueType),
                    ModelName = valueFieldKey,
                    ModelState = bindingContext.ModelState,
                    PropertyFilter = bindingContext.PropertyFilter,
                    ValueProvider = bindingContext.ValueProvider
                };
                object thisValue = valueBinder.BindModel(controllerContext, valueBindingContext);

                // we need to merge model errors up
                AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, valueFieldKey, valueType, thisValue);
                KeyValuePair<object, object> kvp = new KeyValuePair<object, object>(thisKey, thisValue);
                modelList.Add(kvp);
            }

            // if there weren't any elements at all in the request, just return
            if (modelList.Count == 0) {
                return null;
            }

            // replace the original collection
            object dictionary = bindingContext.Model;
            CollectionHelpers.ReplaceDictionary(keyType, valueType, dictionary, modelList);
            return dictionary;
		}

		internal object UpdateCollectionImproved(ControllerContext controllerContext, ModelBindingContext bindingContext, Type elementType)
		{
			IModelBinder elementBinder = Binders.GetBinder(elementType);

			// build up a list of items from the request
			List<object> modelList = new List<object>();
			SortedList<int, int> indices = new SortedList<int, int>();
			int prefixLength = bindingContext.ModelName.Length;
			Regex keyMatch = new Regex("^" + Regex.Escape(bindingContext.ModelName) + @"\[([^\]]+)\]", RegexOptions.IgnoreCase);
			IEnumerable<KeyValuePair<string, ValueProviderResult>> values = new ValueProviderEnumerable(bindingContext.ValueProvider);
			foreach (var entry in values)
			{
				if (entry.Key.Length <= prefixLength)
				{
					continue;
				}
				Match foundKey = keyMatch.Match(entry.Key);
				if (!foundKey.Success)
					continue;
				int currentIndex;
				if ((!int.TryParse(foundKey.Groups[1].Value, out currentIndex)) || indices.ContainsKey(currentIndex))	//A form member of a complex object, already processed in a previous iteration
					continue;
				else
					indices.Add(currentIndex, currentIndex);
			}
			foreach (KeyValuePair<int, int> index in indices)
			{
				int currentIndex = index.Value;
                string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currentIndex);

                ModelBindingContext innerContext = new ModelBindingContext() {
                    ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, elementType),
                    ModelName = subIndexKey,
                    ModelState = bindingContext.ModelState,
                    PropertyFilter = bindingContext.PropertyFilter,
                    ValueProvider = bindingContext.ValueProvider
                };
                object thisElement = elementBinder.BindModel(controllerContext, innerContext);

                // we need to merge model errors up
                AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, subIndexKey, elementType, thisElement);
                modelList.Add(thisElement);
            }

            // if there weren't any elements at all in the request, just return
            if (modelList.Count == 0) {
                return null;
            }

            // replace the original collection
            object collection = bindingContext.Model;
            CollectionHelpers.ReplaceCollection(elementType, collection, modelList);
            return collection;
		}
	}
}
